// -----------------------------------------------------------------------------
// This source file is part of Matrix Platform
// 	(Universal .NET Software Development Platform)
// For the latest info, see http://www.matrixplatform.com
// 
// Copyright (c) 2009-2010, Ingenious Ltd
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Threading;

#if Matrix_Diagnostics
    using Matrix.Common.Diagnostics;
#endif

namespace Matrix.Framework.DataStorage.DataSeries
{
    /// <summary>
    /// Regarding GetItemOffset(A.K.A item seek):
    /// The class uses a simple self adjusting AI code, to try and guess where an item is (where item are variable in size).
    /// So it should slightly improve over time if used intensively; performed in the EvaluateEstimationTargetPrecedingItemsCount().
    /// </summary>
    public class DataSeriesReader : DataSeriesReaderRaw
    {
        IItemSerializer _serializer;

        object _syncRoot = new object();

        #region Item Location Estimation Parameters

        int _currentPrecedingItemsCount = 50;
        double _precedingItemsCountAdjustmentStepRatio = 0.0001d;

        int _estimationHitsAfterRequestedLocation = 0;
        int _estimationHitsBeforeRequestedLocation = 0;

        #endregion

        /// <summary>
        /// When using in the page reading, return false to stop reading further.
        /// </summary>
        /// <returns></returns>
        public delegate bool PagedReadDelegate(DataSeriesReader reader, List<object> data);

        /// <summary>
        /// Constructor.
        /// </summary>
        public DataSeriesReader()
        {
        }

        /// <summary>
        /// 
        /// </summary>
        public override void Dispose()
        {
            _serializer = null;
            base.Dispose();
        }

        /// <summary>
        /// 
        /// </summary>
        public bool Initialize(IItemSerializer serializer, string filePath)
        {
            lock (_syncRoot)
            {
                if (_serializer != null)
                {
#if Matrix_Diagnostics
                    SystemMonitor.OperationError("Reader already initialized.");
#endif
                    return false;
                }

                _serializer = serializer;
            }

            base.Initialize();

            try
            {
                lock (_syncRoot)
                {
                    _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                }
            }
            catch (Exception ex)
            {
#if Matrix_Diagnostics
                SystemMonitor.OperationError("Failed to initialize reader.", ex);
#endif
                if (_fileStream != null)
                {
                    _fileStream.Dispose();
                }

                _serializer = null;
                return false;
            }

            return true;
        }


        /// <summary>
        /// Helper. Throws OperationCanceledException().
        /// </summary>
        /// <returns></returns>
        protected uint GetItemIndexFromBuffer(byte[] buffer, long bufferLength) 
        {
            uint index, payloadSize;
            object dummy;

            if (CreateItemFromBuffer(_serializer, SequenceHelper, buffer, bufferLength, true, 
                                     out index, out payloadSize, out dummy) == false)
            {
                throw new OperationCanceledException("Failed to extract item data.");
            }

            return index;
        }

        /// <summary>
        /// Redefine.
        /// </summary>
        public object ReadLast(uint stepBack)
        {
            uint dummy;
            return ReadLast(stepBack, out dummy);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public object ReadLast(uint stepBack, out uint index)
        {
            index = uint.MaxValue;
            IItemSerializer serializer = _serializer;
            if (serializer == null)
            {
                return null;
            }

            byte[] buffer = RawReadLast(stepBack);

            uint totalSize;
            object item = null;

            if (buffer == null || buffer.Length == 0
                || CreateItemFromBuffer(serializer, SequenceHelper, buffer, buffer.Length, false, out index, out totalSize, out item) == false)
            {
                return null;
            }

            return item;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="offset"></param>
        /// <param name="countPerPage"></param>
        /// <param name="maxCount"></param>
        /// <param name="delegateInstance">The delegate to receive the data "pages".</param>
        /// <returns>Count of totals items read.</returns>
        public int ReadPaged(ref long offset, int maxCountPerPage, int maxCount, bool skipMisformattedItems, 
                             PagedReadDelegate delegateInstance)
        {
            IItemSerializer serializer = _serializer;
            if (serializer == null)
            {
                return 0;
            }

            RawPagedReadDelegate rawDelegateInstance = null;
            if (delegateInstance != null)
            {
                rawDelegateInstance = delegate(DataSeriesReaderRaw readerInstance, List<byte[]> data)
                                          {
                                              List<object> result = new List<object>();

                                              uint index, itemSize;
                                              foreach (byte[] buffer in data)
                                              {
                                                  object item;
                                                  if (CreateItemFromBuffer(serializer, SequenceHelper, buffer, buffer.Length, false, out index, out itemSize, out item) == false)
                                                  {
                                                      if (skipMisformattedItems == false)
                                                      {
                                                          result.Add(null);
                                                      }

#if Matrix_Diagnostics
                            SystemMonitor.OperationError("Failed to create item.");
#endif
                                                      continue;
                                                  }

                                                  result.Add(item);
                                              }

                                              return delegateInstance(this, result);
                                          };
            }

            return RawReadPaged(ref offset, maxCountPerPage, maxCount, rawDelegateInstance);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="skipMisformattedItems">If false - if an item is misformatted, a null will be returned in the list; set to true to signify skip this type of items.</param>
        /// <returns>The items loaded. </returns>
        public List<ItemType> Read<ItemType>(ref long offset, int maxCount, bool skipMisformattedItems)
            where ItemType : class
        {
            IItemSerializer serializer = _serializer;
            if (serializer == null)
            {
                return null;
            }

            List<byte[]> items = RawRead(ref offset, System.IO.SeekOrigin.Begin, maxCount, false);

            List<ItemType> result = new List<ItemType>();

            if (items != null)
            {
                foreach (byte[] buffer in items)
                {
                    uint index, payloadSize;
                    object item;

                    if (CreateItemFromBuffer(serializer, SequenceHelper, buffer, buffer.Length, false,
                                             out index, out payloadSize, out item) == false)
                    {
                        if (skipMisformattedItems == false)
                        {
                            result.Add(null);
                        }

#if Matrix_Diagnostics
                        SystemMonitor.OperationError("Misformatted or unreadable item located, item skipped or null provided.");
#endif
                        continue;
                    }

                    result.Add((ItemType)item);
                }
            }

            return result;
        }

        /// <summary>
        /// How many items to try and hit, before the actual item, to have the best chance of not overshooting.
        /// </summary>
        /// <returns></returns>
        int EvaluateEstimationTargetPrecedingItemsCount(long totalItemCount)
        {
            int adjustmentStep = (int)(_precedingItemsCountAdjustmentStepRatio * totalItemCount);
            if (_estimationHitsAfterRequestedLocation > 4 || _estimationHitsBeforeRequestedLocation > 4)
            {
                double ratio = (double)_estimationHitsAfterRequestedLocation / (double)_estimationHitsBeforeRequestedLocation;

                // We want the ratio to be between 0.2 and 0.35.
                if (ratio > 0.35)
                {// We are looking at ratio 0.2 or less.
                    _currentPrecedingItemsCount += adjustmentStep;
                }

                if (ratio < 0.2)
                {// Decrease, but no less than 10.
                    _currentPrecedingItemsCount = Math.Max(10, _currentPrecedingItemsCount - adjustmentStep);
                }
            }

            return _currentPrecedingItemsCount;
        }

        /// <summary>
        /// Skip to item with this index.
        /// </summary>
        /// <param name="index"></param>
        public long? GetIndexOffset(int index)
        {
            if (index == 0)
            {
                return 0;
            }

            // If within skipLimit items, we shall no longer skip, but read all.
            double skipStepPercentage = 0.5;
            
            // 1MB or less loaded  directly.
            int minimumSkipFileSize = 1024 * 1024;

            try
            {
                if (FileStreamSize < minimumSkipFileSize)
                {// File too small, just read until we find the index.
                    long tempOffset = 0;
                    RawRead(ref tempOffset, SeekOrigin.Begin, index, true);
                    return tempOffset;
                }

                byte[] itemData = RawReadLast(1);
                long lastReadItemIndex = GetItemIndexFromBuffer(itemData, itemData.Length) + 1;
                
                double averageItemSize = (double)FileStreamSize / (double)lastReadItemIndex;
                // First shot - try to hit somewhere near the targer, up to about 50 items before it.
                long expectedIndexPosition = (long)(averageItemSize * Math.Max(0, index - EvaluateEstimationTargetPrecedingItemsCount(lastReadItemIndex)));

                long tempIndexPosition = expectedIndexPosition;
                itemData = RawRead(ref tempIndexPosition, System.IO.SeekOrigin.Begin);
                lastReadItemIndex = GetItemIndexFromBuffer(itemData, itemData.Length) + 1;

                // Keep track of how we perform.
                if (lastReadItemIndex < index)
                {
                    Interlocked.Increment(ref _estimationHitsBeforeRequestedLocation);
                }
                else
                {
                    Interlocked.Increment(ref _estimationHitsAfterRequestedLocation);
                }

                while(lastReadItemIndex > index && expectedIndexPosition > 0)
                {// Overshoot, decrease X%.
                    expectedIndexPosition -= (long)(averageItemSize * (double)lastReadItemIndex / (100d / (double)skipStepPercentage));
                    expectedIndexPosition = Math.Max(0, expectedIndexPosition);

                    tempIndexPosition = expectedIndexPosition;
                    itemData = RawRead(ref tempIndexPosition, System.IO.SeekOrigin.Begin);
                    lastReadItemIndex = GetItemIndexFromBuffer(itemData, itemData.Length) + 1;
                }

                if (lastReadItemIndex > index)
                {
#if Matrix_Diagnostics
                    SystemMonitor.OperationError(string.Format("Failed to track up to item [{0}].", index));
#endif
                    return null;
                }

                if (lastReadItemIndex != index)
                {// Undershoot, read all until we hit the index.
                    RawRead(ref expectedIndexPosition, System.IO.SeekOrigin.Begin, (int)(index - lastReadItemIndex + 1), true);
                    return expectedIndexPosition;
                }
                else
                {
                    return tempIndexPosition;
                }
            }
            catch (OperationCanceledException ex)
            {
#if Matrix_Diagnostics
                SystemMonitor.OperationError(string.Format("Failed to locate item [{0}].", index), ex);
#endif
                return null;
            }
        }


    }
}
